探索章程(Charter)的目標與內容都寫好了,今天讓新同事拿著它,自己決定怎麼探索這個產品。
今天的主角是 explore,它是 model-invoked,agent 遇到「依照這份 charter 去探索」這類任務會自行觸發,名字不用記。
用 charters/toolshop-checkout-payment.yaml 跑一輪探索
但如果你想指定 skill,可以這樣把它明確叫出來。
/sdet-skills:explore 用 charters/toolshop-checkout-payment.yaml 跑一輪探索
如果手上還沒有章程,先照昨天那篇把它生出來。
第三種方式是你可以不寫成 charter 檔。你可以在告訴這位新同事時,把目標、範圍、oracle、邊界四樣直接講給他聽。
這種方式適合一次性、不打算重跑的探索。缺點是下次要重跑得再講一遍,而且沒有一份可以先給人審的檔案。
昨天那份 charter 給了目標、判準、界線,唯獨沒給路。這是刻意的,但也留下一個問題:它憑什麼決定第一步點哪裡?
放著不管,會往兩個方向壞掉,而且兩種都不會報錯。
一種是亂走。 沒有排序依據的話,畫面上有什麼就點什麼,二十五步用完了還在首頁附近打轉,max_steps 到了就交出一份「已探索完畢」。
另一種更難發現,是鬼打牆。 它在購物車跟商品頁之間來回,每一輪都覺得自己在前進,因為它不記得三步之前做過同一件事。人不會這樣,因為人記得走過哪裡;模型的 context 會滿、會被壓縮,跨一輪就沒了。
所以今天要補的是兩件事:一張讓它有依據去挑下一步的表,還有一份動作之前要先查的路徑記憶。少了前者它亂走,少了後者它原地繞圈。
跑 charter 之前,有兩件事要先確認,而且順序不能顛倒。
第一個是設定檔,但這件事有前提:只有 charter 填了 project 欄位,它才會去讀 config/<專案>/ 底下的設定。沒填的 charter 不查設定,網址由 charter 的 target 自己帶,昨天那份就是這種。
填了的話,找不到對應的目錄它會停下來,而不會去讀其他專案的設定。這也符合預期,因為回退去讀別人的設定,等於拿 A 產品的網址和帳號去打 B 產品,而且產出的證據看起來還很正常。
第二個才是帳號密碼,順序在設定檔後面,因為要先知道讀哪一組設定,才知道該準備哪一組憑證。要驗登入或結帳就得先把環境變數備好,不然它走到登入牆就停在那裡。這件事我第一輪真的踩到了。
explore 跑起來之後,每一步固定四個動作:
exploration-log.yaml
為了防止系統會隨機亂跑(例如在沒有理由的情況下執行下一步,或是重複執行曾經做過的操作,甚至可能在缺乏證據的情況下誤報這是 Bug),停止條件和幾條鐵則就得先寫清楚。
所以 skill 裡面直接寫死:沒有理由就不要做下一步,做過的操作不要再做一次。
再來就是,如果沒有證據,那你就不能宣稱它是一個 Bug。至於是不是 Bug,後面會交給另一支 skill 判斷,它叫 test-oracle。
其實這跟我過去在做探索性測試的時候是一樣的步驟。
先知道目標是什麼,接著憑過去的經驗、修過的那些 bug,決定下一步該去戳哪裡。
過程要記流程、留關鍵證據,最後才下結論:這一輪找到的東西,是不是真的潛在缺陷。
不過它跟人有一點不一樣:它不會知道什麼時候該停,所以停止條件得由你設。
例如說:
不過我跑了兩輪下來,發現真正讓它停下來的原因,一次都不是上面這三條。
第一輪停在結帳的登入牆,因為帳號密碼的環境變數沒設,它進不去,只好記成 blocked。第二輪跑到最後一步要確認訂單到底有沒有建立,多花了一步,26 步超過我訂的 25 步上限。
規格上寫得出來的停止條件,跟現場真正會發生的停止原因,是兩件事。所以停手的時候不能只記「我停了」,要記清楚是為什麼停的。
停手的時候還要自我檢查一次:這一輪完成了多少、哪些還沒完成,然後把結果寫進 exploration-log.yaml 的 stop_reason 欄位,連同一份逐條的覆蓋自檢。
上面那段講人怎麼挑:憑經驗、憑修過的 bug。但這位新同事沒有經驗,它昨天才拿到 charter。
所以要給它一張表。專案裡有一份 references/heuristics.md,explore 動手前會讀它。表的形狀固定是「happy path 藏了什麼假設,怎麼把那個假設反過來測」:
| heuristic | 隱含假設 | 反過來怎麼測 |
|---|---|---|
| 逆操作 | 有新增就有還原 | 加了就移除、設了就清空 |
| 非法狀態轉移 | 使用者照步驟順序走 | 跳過前置、直接打後面步驟的網址 |
| 狀態組合 | 每種狀態畫面都正常 | 空與滿、登入與未登入,看同一個畫面 |
| 重複與併發 | 使用者只做一次 | 連點兩次、開兩個分頁 |
| 權限越界 | 只有該看的人會看 | 未登入打需授權的資源、用別人的 id |
| 過期 | 資料永遠新鮮 | 放久了再操作、用過期的 session |
| 中斷 | 使用者會把流程走完 | 填到一半重整、關頁再回來 |
回頭看第 4 步那個「把數量從 1 改成 3」。它不是隨手點的,那屬於狀態組合這一條:同一個畫面在數量 1 和數量 3 兩種狀態下,金額該不該一致。候選動作從表裡來,排序靠 charter 的目標。這輪的目標是金額,所以跟金額有關的變因排前面。
charter 還有一個旋鈕可以直接影響它的選擇:tours 欄位。填 tours: [authz] 等於指定它戴上權限這副眼鏡去看整個產品。昨天那份 charter 沒填,所以它用的是通用那七條。
這份 reference 還有一條紀律:每次挑戰之前要先點名打的是哪個假設,寫進 log 的 assumption 欄。老實說,我跑的那兩輪 log 都沒有這一欄,理由寫在 why 裡但沒有點名假設。規格寫了、實作沒跟上,這種落差自己要知道。
四個動作裡的第 4 步只講了一半。完整的規矩是動作之前先查 log,動作之後才寫回。
這半邊才是重點。只寫不查,log 就只是一份事後報告;記憶之所以擋得住鬼打牆,是因為它在動作之前被讀,不是因為它在動作之後被寫。
那「走過」的單位是什麼?畫面側記的是走過的頁面與試過的操作。端點側不一樣,記的是「端點加方法加這次改動的變因」,同一個端點換一個變因算新的一步,重打一個完全相同的請求不算。這個定義就是「重複」的判準,判準訂得鬆,防重複就形同虛設。
還有一個問題值得問:模型不是有 context 嗎,為什麼還要多寫一個 yaml?
因為 context 會滿、會被壓縮,跨一輪就沒了。落地成檔案有三個好處:這一輪跑到一半中斷可以續跑、下一輪可以先讀上一輪走過哪裡、而且人看得懂,審得動。
探索過程自己產生的東西,會存在下列這幾個位置
output/sessions/<日期>_<slug>/exploration-log.yaml 走過的路徑
output/sessions/<日期>_<slug>/findings/F-NNN-*.yaml 一筆一檔的候選發現
output/evidence/<YYYYMMDD>-<slug>/ 截圖、console、network
走過的路徑、每一步的理由,還有這一輪產出哪幾筆 finding,全部記在 exploration-log.yaml。今天早上那輪的第 4 步長這樣:
- n: 4
where: "#/product/1"
why: "數量是金額計算最短的變因,先把 1 改成 3 再加入購物車"
action: "click [data-test=increase-quantity] ×3"
observed: "quantity 仍為 1 —— 三次點擊都沒改變值"
leads_to: F-002
charter 的 scope 只寫了「商品頁」,沒有寫要改數量。挑單價最好算的商品、把數量從 1 改成 3,是它自己決定的,why 那一格就是它的理由。這就是 action selection。
再往下幾步,它做了一件更有意思的事:
- n: 9
why: "換一個變因看行小計會不會跟著動"
action: "fill [data-test=product-quantity] = 2"
observed: "line 仍 $00.00,total 仍停在 $42.45 —— 疑似總計未重算"
- n: 10
why: "『改完立刻讀』可能讀在重算之前,補一次 blur 走到終態再讀"
action: "press Tab,等 3 秒後重讀"
observed: "total 變成 $28.30 = 2 × 14.15。**總計是對的,第 9 步的懷疑排除**"
verdict: "捨棄假設 —— 總計未重算是我讀太早,不是產品的錯"
dropped_hypotheses:
- what: "改購物車數量後總計不重算"
why_dropped: "第 10 步 blur 後總計正確變成 $28.30。是我在重算前讀值,不是缺陷"
第 9 步它以為抓到一個 bug,第 10 步自己把它撤回,而且撤回的理由留在檔案裡。這是路徑記憶的另一半用途:不只記「走過哪裡」,也記「哪一條路已經證明是死路」,下一輪才不會再懷疑一次。
至於第 4 步撞到的那個「按鈕點了沒反應」,會變成一筆 finding,一筆一檔:

同一個缺陷在 2026-07-30 那輪留下的截圖。畫面上看不出它壞了:按鈕在、數字在,只是按了不動,所以它才需要「點三次再讀值」這種動作才抓得到。
id: F-002
title: "商品頁的數量 +/− 按鈕點了沒反應,只有直接輸入才改得動"
status: anomaly
oracle: user-expectation
oracle_detail: >
charter 的四條 oracle 沒有一條直接涵蓋這個行為,所以標 anomaly 不標 fail。
判定要靠「使用者期待」這條 oracle,交 test-oracle 定奪。
confidence: 0.85
evidence:
- "click ×3 後 eval 讀 value:\"1\"(含一次用 ref、兩次用 data-test 選擇器,結果一致)"
- "同一欄位 fill \"3\" 後 eval 讀 value:\"3\" —— 欄位本身沒鎖,是按鈕沒作用"
- "點擊期間沒有新的 console error,失敗是靜默的"
注意 status 是 anomaly 不是 fail。charter 的四條 oracle 沒有一條管得到「加號按鈕該不該加一」,所以它只記「看到了、覺得怪」,不敢定罪。沒有 oracle 就不下結論,這條規矩在這裡看得最清楚。
每一步固定四個動作:觀察、決定(理由要引當下畫面的具體證據)、執行、寫回 log;而完整的規矩是動作之前先查 log,動作之後才寫回,記憶擋得住鬼打牆是因為它在動作之前被讀。候選動作從 references/heuristics.md 那七條來,排序靠 charter 的目標,tours 欄位可以指定它戴哪一副眼鏡。停手不能只記「我停了」,要記為什麼停,因為規格上寫得出來的停止條件跟現場真正發生的停止原因是兩件事。
charter(目標/判準/界線)
│
▼
┌─────── 每一步四個動作 ───────┐
│ │
▼ │
① 觀察現況 │
│ │
▼ │
② 決定下一步 ◄──── references/heuristics.md
理由要引證據 七條啟發式 + charter 的 tours
│ │
▼ │
先查 log:走過了嗎? ────► 走過 → 換一個 │
│ │
▼ │
③ 執行 action │
│ │
▼ │
④ 寫回 exploration-log.yaml ────────────┘
│
▼
停手時:stop_reason + 覆蓋自檢
(實跑兩輪:一次 blocked、一次超一步,
都不是規格上寫的那三條)
│
▼
exploration-log / findings / evidence
走過的路|候選發現|截圖 console network
│
▼
dropped_hypotheses:死路也要留
下一輪才不會再懷疑一次同一件事
記憶要在動作之前被讀,只寫不查的話 log 就只是一份事後報告,擋不住鬼打牆。撤回的假設跟找到的缺陷一樣值錢,第 9 步的懷疑被第 10 步推翻,理由留在 dropped_hypotheses 裡,那是可審性不是自我檢討。而沒有 oracle 就不下結論,管不到的行為只能記 anomaly,定罪是明天的事。
找到這些 findings,還不能直接開票給 developer。
接下來還有三關:
把這些 findings 做分類
判別這些分類完的 findings 是不是 bug
判斷這些 issue 是不是已經開過了,或者其實沒辦法重現
最後,確認無誤的才會開單給 developer。
明天先處理第一關的前半段:那七條啟發式今天只列了表,還沒真的拿去戳產品。別只測 happy path,明天帶它挑戰那些「使用者應該不會這樣做」的假設。
exploration-log.yaml 的觀念源頭references/heuristics.md - 那七條與 tours 欄位的真檔skills/explore/explore/ - 四個動作與停止條件的實作docs/explore/explore.md - 為什麼路徑記憶要落地成檔案